云鸽笔记|技术复盘与总结
点击「京东数科技术说」可快速关注
「摘要」在日常工作和生活中,写笔记、做文件备份都是非常重要的活动,我们也本着这样的想法,搭建了在公司内部场景下使用的笔记平台——云鸽笔记。如笔记平台的名字一般,希望平台上的笔记能给我们的工作带来更多便捷的信息共享、思想碰撞。
本着信息共享、实现协作的想法,我们从用户使用场景和习惯出发,分别实现了便于工作时间快速操作的PC端、方便非工作时间快速查阅工作信息的M端,以及用来实现多人员协作的云协作。
一、生态
云协作:多人员协作、项目资源共享、信息同步、共享编辑、周报管理等; 云鸽笔记PC端:工作时间、快速操作、随时备忘; 云鸽笔记M端:非工作时间、快速查阅、随时备忘、操作轻量。
二、生态图景
云协作、云鸽笔记PC端
云鸽笔记M端
三、生态路引
云鸽笔记PC端:https://yunge.jd.com 云鸽笔记M端:进入“京ME”APP,应用下搜索“云鸽笔记”。如无法查到,联系@tongen。 云协作:https://yunge.jd.com/team
践行敏捷
一、功能模块
二、框架设计
三、图片格式的选择
相比传统的图片,尺寸更小,可压缩性更强
可伸缩,更清晰
方便读取和修改
设计软件直接导出
一、多个svg使用问题
svg的使用方式多种多样,适合自己的才是最好的。下面简单介绍下我们的项目如何在jdwtool脚手架中使用了svg。因为jdwtool基于webpack打包,所以webpack中必不可少需要增加针对svg的配置。代码如下:
// webpack.config.js
{
test: /\.svg$/,
use: [
{
loader: path.resolve(__dirname, "./fdtsvgloader")
},
"svg-loader"
],
include: [path.resolve(opts.baseDir, "src")],
exclude: [path.resolve(opts.baseDir, "src/svginline")]
}
我们依然使用svg-loader进行svg的处理,但svg-loader返回的是一个包含attributes
和content
的对象,无法直接使用。处理后的结果如下代码所示:
module.exports = {
attributes: {
xmlns: 'http://www.w3.org/2000/svg',
viewBox: '0 0 1024 1024'
},
content: '<path d="M441.9 167.3l-19.8-19.8c-4.7-4.7-12.3-4.7-17 0L224 328.2 42.9 147.5c-4.7-4.7-12.3-4.7-17 0L6.1 167.3c-4.7 4.7-4.7 12.3 0 17l209.4 209.4c4.7 4.7 12.3 4.7 17 0l209.4-209.4c4.7-4.7 4.7-12.3 0-17z"/>'
}
因此,在fdt中单独写了一个loader来得到想要的svg格式。代码如下:
// fdtsvgloader.js
module.exports = function(source) {
return `
${source}
var fdtsvg = require('fdt-svg-loader')
module.exports = fdtsvg(module.exports)
`;
};
// fdt-svg-loader
var React = require("react");
module.exports = function(svg) {
const content = svg.content;
return function(props) {
const newprops = { viewBox: "0 0 1024 1024", height: "20px", fill: "#000" };
newprops.dangerouslySetInnerHTML = { __html: content };
newprops;
return React.createElement("svg", { ...newprops, ...props });
};
};
其中,fdtsvgloader.js中接受的参数即为svg-loader处理后的结果。最后,经过fdt-svg-loader处理后,得到了React创建的svg元素,并包含默认属性viewBox,height以及fill值。因此我们在组件中可以如下方式引用svg:
// demo.tsx
import PptIcon from "@/image/newppt.svg";
export default class Demo extends Component {
render() {
return <PptIcon width="18" height="18" viewBox="0 0 27 34" />;
}
}
demo中传递的属性便可覆盖默认属性,灵活控制svg的大小。至此,我们在项目中愉快的使用svg来控制各式各样图标的显示。
<g>
该标签代表组合<defs>
定义重用图形<polygon>
定义多边形<mask>
定义蒙层<use>
实现SVG现有图形的重用
既然无法直接找到答案,那只好上排除法来寻找问题所在了。最后发现,问题出现的原因是新引入的图标影响了原有图标。svg互相影响也真的让我非常震惊。
那到底是怎么互相影响的呢?原因就是新的图标中定义了一个mask蒙层,属性id为mask-2
。受影响的图标中,path标签的mask属性引用了该mask-2
的蒙层,导致新图标的出现影响了部分旧图标。
那么对于直接在html中引入svg,浏览器对于重用图标的寻找机制是怎么样的呢?我们做了如下测试:
<svg width="400" height="300">
<defs>
<linearGradient id='white2black'>
<stop offset="0" stop-color="white"></stop>
<stop offset="100%" stop-color="black"></stop>
</linearGradient>
<mask id="opacity">
<rect x="0" y="0" width="400" height="300" fill="url(#white2black)"></rect>
</mask>
</defs>
<rect id="back" x="0" y="0" width="400" height="300" fill="#d4fcff"></rect>
<rect id="front" x="0" y="0" width="400" height="300" fill="#fcd3db" mask="url(#opacity)"></rect>
</svg>
<svg width="600" height="300">
<defs>
<linearGradient id='white2black'>
<stop offset="0" stop-color="blue"></stop>
<stop offset="50%" stop-color="black"></stop>
<stop offset="100%" stop-color="green"></stop>
</linearGradient>
<mask id="opacity">
<rect x="50" y="0" width="600" height="400" fill="green"></rect>
</mask>
</defs>
<rect id="back" x="0" y="0" width="400" height="300" fill="#d4fcff"></rect>
<rect id="front" x="0" y="0" width="400" height="300" fill="#fcd3db" mask="url(#opacity)"></rect>
</svg>
该代码的展示效果为:
当把第一个svg的mask标签删除之后
<svg width="400" height="300">
<defs>
<linearGradient id='white2black'>
<stop offset="0" stop-color="white"></stop>
<stop offset="100%" stop-color="black"></stop>
</linearGradient>
</defs>
<rect id="back" x="0" y="0" width="400" height="300" fill="#d4fcff"></rect>
<rect id="front" x="0" y="0" width="400" height="300" fill="#fcd3db" mask="url(#opacity)"></rect>
</svg>
<svg width="600" height="300">
<defs>
<linearGradient id='white2black'>
<stop offset="0" stop-color="blue"></stop>
<stop offset="50%" stop-color="black"></stop>
<stop offset="100%" stop-color="green"></stop>
</linearGradient>
<mask id="opacity">
<rect x="50" y="0" width="600" height="400" fill="green"></rect>
</mask>
</defs>
<rect id="back" x="0" y="0" width="400" height="300" fill="#d4fcff"></rect>
<rect id="front" x="0" y="0" width="400" height="300" fill="#fcd3db" mask="url(#opacity)"></rect>
</svg>
效果变为:
二、第三方库引用新姿势
项目中依然使用:
一次加载,多处使用。加载一次cdn上的react文件,任何请求该文件的项目都可走缓存,而无需请求。
方便升级react版本。大家都知道,react总会给我们带来很多惊喜,尝试新版本总是变的迫不及待。使用这种方法,可以轻松升级react版本,而无需担心重新打包vendor。
脚手架安装依赖,项目初始化更快。很多的依赖都安装在了脚手架中,那么项目中安装的依赖变的非常少,这使得每个人clone下项目后,无需花费过多的时间等待依赖的安装。
脚手架统一升级依赖版本。依赖都安装在脚手架的安装目录下,当需要升级依赖版本时,只需要更新脚手架即可。
三、antd icons 按需加载
一、滚动列表的实现
<InfiniteLoader
isRowLoaded={isRowLoaded}
loadMoreRows={loadmore}
rowCount={count}
threshold={10}
minimumBatchSize={20}
>
{({ onRowsRendered, registerChild }) => (
<AutoSizer>
{({ height, width }) => (
<List
ref={registerChild}
onRowsRendered={onRowsRendered}
deferredMeasurementCache={cache}
rowHeight={cache.rowHeight}
rowRenderer={rowRenderer}
/>
)}
</AutoSizer>
)}
</InfiniteLoader>
const rowRenderer = ({ key, index, style, parent }) => {
return (
<CellMeasurer
cache={cache}
columnIndex={0}
key={key}
parent={parent}
rowIndex={index}
>
<div
className="infinitelist-item"
>
</div>
</CellMeasurer>
)
};
二、列表页面切换保持切换前滚动高度
// 写
setScrollTopByPager(page: string, top: number) {
this.topHash[page] = top;
};
// 读
getScrollTopByPage(page: string) {
return this.topHash[page];
};
// 侦听滚动
scrollNode.addEventListener('scroll', () => {
this.setScrollTopForRoutor(
this.pageId,
scrollNode.scrollTop
);
})
// 再次进入页面时读取并设置
let top = this.getScrollTopByPage(pageId);
scrollNode.scrollTo(0, top);
三、笔记的编辑和预览
四、MD 和富文本编辑
iOS:iOS的键盘在窗口的最上层,当键盘弹起时,webview的高度并不会发生变化,但是scrollTop会发生变化,页面发生滚动,而页面可滚动的最大高度是弹出的键盘的高度;且只有当键盘弹起时页面滚动到底部时, scrollTop 的变化值才为键盘高度。
Android:webview留出键盘空间,键盘弹起时页面高度会改变,内容区域会减少,页面不会发生滚动,flex布局下会压缩页面。
input.scrollIntoView(true);
input.scrollIntoViewIfNeeded();
另外我们希望笔记标题在顶部固定,内容区域和可编辑区域可滚动,因此采用了flex布局。但场景中可编辑区已经处于可视区域,所以并没能解决问题,还有重要的一点是键盘弹起之后编辑区域也是可滚动的,依然会有无法滚动到底部的问题,键盘还是会遮挡一部分内容。
let noteHeader = document.querySelector('.note-header');
window.addEventListener('scroll', this.scrollFn, false);
this.scrollFn = () => {
let positionTop = document.body.scrollTop;
noteHeader.style.top = positionTop + 'px';
}
这样能让标题固定在顶部,由js动态控制,体验上差一些。键盘遮挡内容的问题还是没有解决,经过反复的实验,发现其实整个内容区域并没有滚动上去,键盘下面的位置还有一部分内容,因此需要在键盘弹起的时候将内容向上顶起,使这部分内容处于键盘上方,也就是在键盘弹起后给内容区域加 paddingBottom
属性,键盘下面区域用空白填充,内容即可滚动到键盘上方,先来实验一下。
let focusinFn = () => {
let content = document.querySelector('.content');
content.style.paddingBottom = '7.5rem';
}
window.addEventListener('focusin', focusinFn, false);
paddingBottom
值来实现这个效果,键盘收起的时候重置成初始的状态,后续优化这快高度的设置。五、PDF 和 Office 文件预览
embed
标签加载;而Office文件使用了 iframe
加载。但无法滚动和缩放,使阅读体验大打折扣。后来我们改进了此方法,先下载文件,再使用手机的文件预览功能打开文档进行预览,实验成功。而收获的意外体验是,这时可以友好的提示用户是否要下载当前文档,给了用户更多的控制权。axios({
method: "get",
url: `downloadurl`,
responseType: "blob",
onDownloadProgress: (progressEvent) => {
onDownloadProgress && onDownloadProgress(progressEvent);
}
}).then(function(response) {
var a = document.createElement("a");
var url = window.URL.createObjectURL(response.data);
a.href = url;
a.download = name;
a.click();
window.URL.revokeObjectURL(url);
});
六、与京ME应用的打通
京东数科技术说&技术课堂
▼▼▼
由京东数科-数字技术中心策划组织
倡导“原创·实用·技术·专业”
致力于分享技术领域实战经验与技术干货
线上订阅“京东数科技术说”,线下聆听“技术课堂”
为加强技术分享、总结沉淀,提升数科技术影响力而搭建的
线上线下融合交流平台
咨询、建议、合作请联系:
刘嘉璐(liujialu)/张明瑛(zhangmingying3)
长按识别二维码关注我们